/***************************** BEGIN LICENSE BLOCK ***************************

 The contents of this file are subject to the Mozilla Public License Version
 1.1 (the "License"); you may not use this file except in compliance with
 the License. You may obtain a copy of the License at
 http://www.mozilla.org/MPL/MPL-1.1.html
 
 Software distributed under the License is distributed on an "AS IS" basis,
 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 for the specific language governing rights and limitations under the License.
 
 The Original Code is the "Space Time Toolkit".
 
 The Initial Developer of the Original Code is the VAST team at the
 University of Alabama in Huntsville (UAH). <http://vast.uah.edu>
 Portions created by the Initial Developer are Copyright (C) 2007
 the Initial Developer. All Rights Reserved.
 
 Please Contact Mike Botts <mike.botts@uah.edu> for more information.
 
 Contributor(s): 
    Alexandre Robin <robin@nsstc.uah.edu>
 
******************************* END LICENSE BLOCK ***************************/

package org.vast.stt.renderer.opengl;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import javax.media.opengl.GL;
import javax.media.opengl.glu.GLU;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.vast.ows.sld.Symbolizer;
import org.vast.stt.style.DataStyler;
import org.vast.stt.style.GridPatchGraphic;
import org.vast.stt.style.RasterPixelGraphic;
import org.vast.stt.style.RasterTileGraphic;
import org.vast.stt.style.TextureStyler;
import org.vast.util.MessageSystem;


/**
 * <p><b>Title:</b><br/>
 * Texture Manager
 * </p>
 *
 * <p><b>Description:</b><br/>
 * Generate POT or NPOT textures according to OpenGL hardware 
 * capabilities. POT textures are generated by padding with 100%
 * transparent white pixels, in which case texture coordinates
 * are automatically adjusted.
 * </p>
 *
 * <p>Copyright (c) 2007</p>
 * @author Alexandre Robin
 * @date Apr 13, 2006
 * @version 1.0
 */
public class TextureManager
{
    protected Log log = LogFactory.getLog(TextureManager.class);
    
    protected static Hashtable<Symbolizer, GLTextureTable> symTextureTables
               = new Hashtable<Symbolizer, GLTextureTable>();
    protected Hashtable<Symbolizer, Boolean> symTexturePoolSizeReachedTable = new Hashtable<Symbolizer, Boolean>();
    protected Hashtable<Symbolizer, LinkedList<Integer>> symTextureStackTable = new Hashtable<Symbolizer, LinkedList<Integer>>();
    protected GL gl;
    protected GLU glu;
    protected boolean forceNoExt = false;
    protected boolean npotSupported;
    protected boolean normalizationRequired;
    protected int maxSize = 512;
    protected int maxWastedPixels = 100;
    
    
    class GLTexture
    {
        protected int id = -1;
        protected boolean needsUpdate = true;
        protected int widthPadding;
        protected int heightPadding;
        protected List<GLTexture> tiles;
    }
    
    
    class GLTextureTable extends Hashtable<Object, GLTexture>
    {
        private final static long serialVersionUID = 0;
    }
    
    
    public TextureManager(GL gl, GLU glu)
    {
        this.gl = gl;
        this.glu = glu;
        
        // find out which texture 2D target to use
        String glExtensions = gl.glGetString(GL.GL_EXTENSIONS);
        if (!forceNoExt && glu.gluCheckExtension("GL_ARB_texture_rectangle", glExtensions) ||
            glu.gluCheckExtension("GL_EXT_texture_rectangle", glExtensions))
        {
            OpenGLCaps.TEXTURE_2D_TARGET = GL.GL_TEXTURE_RECTANGLE_EXT;
            log.info("NPOT textures supported");
            npotSupported = true;
            normalizationRequired = false;
        }
        else
        {
            OpenGLCaps.TEXTURE_2D_TARGET = GL.GL_TEXTURE_2D;
            log.info("NPOT textures NOT supported. Textures will be padded with transparent pixels");
            npotSupported = false;
            normalizationRequired = true;
        }
        
        // enable right texture target
        gl.glEnable(OpenGLCaps.TEXTURE_2D_TARGET);
        
        // Display max texture size
        int[] size = new int[1];
        gl.glGetIntegerv(GL.GL_MAX_TEXTURE_SIZE, size, 0);
        log.info("Maximum texture size is " + size[0]);
        this.maxSize = size[0];
    }
    
    
    /**
     * Retrieves stored textureID or create a new one along
     * with the corresponding texture in OpenGL memory.
     * @param styler
     * @param tex
     * @param force
     * @return
     */
    public GLTexture getTexture(TextureStyler styler, RasterTileGraphic tex, boolean force)
    {
        GLTexture texInfo = null;
        Symbolizer sym = styler.getSymbolizer();
        
        synchronized (symTextureTables)
        {
            // try to find table for this symbolizer
            GLTextureTable textureTable = symTextureTables.get(sym);
            
            // create if it doesn't exist
            if (textureTable == null)
            {
                textureTable = new GLTextureTable();
                symTextureTables.put(sym, textureTable);
            }
                
            // try to find texture for this tile
            texInfo = textureTable.get(tex.block);
            
            // create if it doesn't exist
            if (texInfo == null)
            {
                texInfo = new GLTexture();
                textureTable.put(tex.block, texInfo);
            }            
            
            // create new texture if it needs update
            if (texInfo.needsUpdate || force)
            {
                texInfo.needsUpdate = false;
                createTexture(styler, tex, texInfo);
                if (log.isDebugEnabled())
                    log.debug("Tex #" + texInfo.id + " created for block " + tex.block);
                logStatistics();
            }
            
            return texInfo;
        }
    }
    
    
    /**
     * Bind texture
     * @param tex
     * @return
     */
    public void useTexture(GLTexture texInfo, RasterTileGraphic tex)
    {
        // otherwise just bind existing one
        if (texInfo.id > 0)
        {
            if (!gl.glIsTexture(texInfo.id))
            	log.debug("Tex #" + texInfo.id + " DOESN'T EXIST for block " + tex.block);
            else
            {
                gl.glBindTexture(OpenGLCaps.TEXTURE_2D_TARGET, texInfo.id);
                //log.debug("Tex #" + texInfo.id + " used");
            }
        }
        
        // transfer padding info to RasterTileGraphic
        tex.heightPadding = texInfo.heightPadding;
        tex.widthPadding = texInfo.widthPadding;
    }
    
    
    /**
     * Creates a new texture by transfering data from styler to GL memory
     * @param styler
     * @param tex
     * @param texInfo
     */
    protected void createTexture(TextureStyler styler, RasterTileGraphic tex, GLTexture texInfo)
    {
        // fetch texture data from styler
        fillTexData(styler, tex, texInfo);
        Symbolizer sym = styler.getSymbolizer();
        
        // if texture was successfully constructed, bind it with GL
        if (tex.hasRasterData)
        {
        	// size of texture pool
        	int texPoolSize = styler.getSymbolizer().getTexPoolSize();

            // create new texture name and bind it
            int[] id = new int[1];
            
            boolean lastNewTexture = false;
            // ACCORDING TO WHETHER THE TEXTURE POOL SIZE HAS BEEN REACHED
            // A NEW TEXTURE IS GENERATED OR THE FIRST OF THE STACK IS REUSED
            // FOR THE MOST RECENT RASTER DATA
            if((symTexturePoolSizeReachedTable == null) || !symTexturePoolSizeReachedTable.containsKey(sym))
            {
            	// create new texture name
            	gl.glGenTextures(1, id, 0); 
            	if(texPoolSize>0)
            	{
            		if(id[0]<(texPoolSize+1))
            		{
            			if(!symTextureStackTable.containsKey(sym))
            			{
            				LinkedList<Integer> TexIdStack = new LinkedList<Integer>();
            				TexIdStack.addLast(id[0]);
            				symTextureStackTable.put(sym, TexIdStack);
            			}
            			else
            			{
            				LinkedList<Integer> stack = symTextureStackTable.get(sym);
            				stack.addLast(id[0]);
            				symTextureStackTable.put(sym, stack);
            				if(id[0]==texPoolSize)
            				{
            					symTexturePoolSizeReachedTable.put(sym, true); 
            					lastNewTexture = true;
            				}
            					
            			}
            		}
            	}
            }
            else if ((symTexturePoolSizeReachedTable != null) || symTexturePoolSizeReachedTable.containsKey(sym))
            {
            	LinkedList<Integer> stack = symTextureStackTable.get(sym);
	           	id[0] = stack.poll();
	           	stack.addLast(id[0]);        	 
            }
            
            // Bind the texture to the texture Id
            gl.glBindTexture(OpenGLCaps.TEXTURE_2D_TARGET, id[0]);
            
            // set texture parameters
            // TODO:  Allow user to select between Linear (smoothed) and nearest-neighbor interp
            // gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
            // gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
            gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
            gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
            gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
            gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);
            //gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_BORDER);
            //gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_BORDER);
            //gl.glTexParameterfv(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_BORDER_COLOR, new float[] {0.0f,0.0f,0.0f,0.0f}, 0);
                        
            // figure out image format
            int format = 0;
            switch (tex.bands)
            {
                case 1:
                    format = GL.GL_LUMINANCE;
                    break;
                    
                case 2:
                    format = GL.GL_LUMINANCE_ALPHA;
                    break;
                
                case 3:
                    format = GL.GL_RGB;
                    break;
                    
                case 4:    
                    format = GL.GL_RGBA;
                    break;                
            }
            
            gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1);
            
            // create texture in GL memory            
            if(!symTexturePoolSizeReachedTable.containsKey(sym) || lastNewTexture)
            {
            	
            	gl.glTexImage2D(OpenGLCaps.TEXTURE_2D_TARGET, 0, tex.bands,
            					tex.width + texInfo.widthPadding, tex.height + texInfo.heightPadding,
            					0, format, GL.GL_UNSIGNED_BYTE, tex.rasterData);
            }
            else
            {
            	gl.glTexImage2D(OpenGLCaps.TEXTURE_2D_TARGET, 0, tex.bands,
            					tex.width + texInfo.widthPadding, tex.height + texInfo.heightPadding,
            					0, format, GL.GL_UNSIGNED_BYTE, tex.rasterData);
            //	gl.glTexSubImage2D(OpenGLCaps.TEXTURE_2D_TARGET, 0, 0, 0,
            //		               tex.width + texInfo.widthPadding, tex.height + texInfo.heightPadding,
            //		               format, GL.GL_UNSIGNED_BYTE, tex.rasterData);
            }

            // erase temp buffer
            tex.rasterData = null;
            
            // set new id and reset needsUpdate flag
            int oldID = texInfo.id;
            texInfo.id = id[0];
            
            // delete previous texture if needed
            if (oldID > 0)
            {
                gl.glDeleteTextures(1, new int[] {oldID}, 0);
                if (log.isDebugEnabled())
                    log.debug("Tex #" + oldID + " deleted and replaced by " + texInfo.id);
            }      
        }
    }
    
    
    /**
     * Clears all textures used by this symbolizer
     * @param sym
     */
    public void clearTextures(DataStyler styler)
    {
        synchronized (symTextureTables)
        {
            Symbolizer sym = styler.getSymbolizer();
            GLTextureTable textureTable = symTextureTables.get(sym);
            
            if (textureTable != null)
            {
                Enumeration<GLTexture> textureEnum = textureTable.elements();
                while (textureEnum.hasMoreElements())
                {
                    GLTexture texInfo = textureEnum.nextElement();
                    if (texInfo.id > 0)
                    {
                        gl.glDeleteTextures(1, new int[] {texInfo.id}, 0);
                        if (log.isDebugEnabled())
                            log.debug("Tex #" + texInfo.id + " deleted for styler " + styler);
                    }
                    
                    textureTable.remove(texInfo);
                }
                
                symTextureTables.remove(sym);
            }
            
            logStatistics();
        }
    }
    
    
    /**
     * Clears texture used by this symbolizer and
     * associated with the given objects
     * @param sym
     * @param obj
     */
    public void clearTextures(DataStyler styler, Object[] objects)
    {
        synchronized (symTextureTables)
        {
            Symbolizer sym = styler.getSymbolizer();
            GLTextureTable textureTable = symTextureTables.get(sym);
            
            if (textureTable != null)
            {
                for (int i=0; i<objects.length; i++)
                {
                    GLTexture texInfo = textureTable.get(objects[i]);
                    if (texInfo != null)
                    {
                        textureTable.remove(objects[i]);
                        if (texInfo.id > 0)
                        {
                            gl.glDeleteTextures(1, new int[] {texInfo.id}, 0);
                            if (log.isDebugEnabled())
                                log.debug("Tex #" + texInfo.id + " deleted for block " + objects[i]);
                        }
                    }
                    else
                    	log.debug("Texture not found for block " + objects[i]);
                }
            }
            
            logStatistics();
        }
    }
    
    
    /**
     * Create a texture based on data passed by styler
     * @param styler
     * @param tex
     * @param texInfo
     */
    protected void fillTexData(TextureStyler styler, RasterTileGraphic tex, GLTexture texInfo)
    {
        int paddedWidth = tex.width;
        int paddedHeight = tex.height;
        int initialWidth = tex.width;
        int initialHeight = tex.height;
                
        // handle case of padding for npot
        if (!npotSupported)
        {
            // determine closest power of 2
            paddedWidth = closestHigherPowerOfTwo(initialWidth);
            paddedHeight = closestHigherPowerOfTwo(initialHeight);          
            
            // display warning message if padding is needed
            if (paddedWidth != initialWidth || paddedHeight != initialHeight)
            {
                MessageSystem.display("Texture will be padded to have a power of two size.\n" +
                                      "    initial size: " + initialWidth + " x " + initialHeight + "\n" +
                                      "     padded size: " + paddedWidth + " x " + paddedHeight, false);
                
                texInfo.widthPadding = paddedWidth - initialWidth;
                texInfo.heightPadding = paddedHeight - initialHeight;
            }
            
            if (log.isDebugEnabled())
                log.debug("Creating " + paddedWidth + " x " + paddedHeight + " Texture with " + tex.bands + " bands");
        }
        
        // create byte buffer of the right size
        ByteBuffer buffer = ByteBuffer.allocateDirect(paddedWidth*paddedHeight*tex.bands);
        int index = 0;
        
        for (int j=0; j<initialHeight; j++)
        {
            for (int i=0; i<initialWidth; i++)
            {
                RasterPixelGraphic pixel = styler.getPixel(i + tex.xPos, j + tex.yPos);
                buffer.put(index, (byte)pixel.r);
                index++;
                
                // only if RGB
                if (tex.bands > 2)
                {
                    buffer.put(index, (byte)pixel.g);
                    index++;
                    buffer.put(index, (byte)pixel.b);
                    index++;
                }
                
                // only if RGBA
                if (tex.bands == 2 || tex.bands == 4)
                {
                    buffer.put(index, (byte)pixel.a);
                    index++;
                }
            }
            
            // skip padding bytes
            index += texInfo.widthPadding*tex.bands;
        }
        
        tex.rasterData = buffer;
        tex.hasRasterData = true;
    }
    
    
    /**
     * Split a Grid in several tiles with dimensions
     * equal to full powers of two
     * @param tex
     * @return
     */
    protected List<GridPatchGraphic> splitGrid(GridPatchGraphic tex, List<RasterTileGraphic> rasterTiles)
    {
        return null;
    }
    
    
    /**
     * Split a Raster in several tiles with dimensions
     * equal to full powers of two
     * @param tex
     * @return
     */
    public List<RasterTileGraphic> splitTexture(RasterTileGraphic tex)
    {
        List<Integer> widthList =  getPower2SizeList(tex.width);
        List<Integer> heightList =  getPower2SizeList(tex.height);
        int xSegs = widthList.size();
        int ySegs = heightList.size();
        
        List<RasterTileGraphic> tileList = new ArrayList<RasterTileGraphic>(xSegs*ySegs);
        int dX = 0;
        int dY = 0;
        
        for (int i=0; i<xSegs; i++)
        {
            int width = widthList.get(i);
            int widthPadding = 0;
            if (i == xSegs-1)
                widthPadding = width - (tex.width - dX);
                
            dY = 0;
            for (int j=0; j<ySegs; j++)
            {
                RasterTileGraphic nextTile = new RasterTileGraphic();
                int height = heightList.get(j);
                int heightPadding = 0;
                if (j == ySegs-1)
                    heightPadding = height - (tex.height - dY);
                
                nextTile.width = width;
                nextTile.height = height;
                nextTile.xPos = dX;
                nextTile.yPos = dY;
                nextTile.widthPadding = widthPadding;
                nextTile.heightPadding = heightPadding;
                dY += height;
                
                tileList.add(nextTile);
            }

            dX += width;
        }
        
        for (int i=0; i<tileList.size(); i++)
        {
            RasterTileGraphic t = tileList.get(i);
            //GridPatchGraphic g = gridList.get(i);
            
            if (log.isDebugEnabled())
            {
                log.debug("Tile: " + t.width + "x" + t.height + " @ " +
                                   t.xPos + "," + t.yPos + " pad " +
                                   t.widthPadding + "x" + t.heightPadding);
            }
        }
        
        return tileList;
    }
    
    
    /**
     * Breaks done the argument into a list of power of two values
     * @param size
     * @return
     */
    protected List<Integer> getPower2SizeList(int size)
    {
        List<Integer> sizeList = new ArrayList<Integer>();
        int remainSize = size;
        boolean done = false;

        do
        {
            int nextSize = closestHigherPowerOfTwo(remainSize);
            int wastedPixels = nextSize - remainSize;
            
            if (nextSize <= maxSize && wastedPixels <= maxWastedPixels)
            {
                done = true;
            }
            else
            {
                nextSize = closestLowerPowerOfTwo(remainSize);
                remainSize = remainSize - nextSize;
            }

            sizeList.add(nextSize);
        }
        while(!done);

        return sizeList;
    }
    
    
    /**
     * Calculate closest power of two value lower than argument
     * @param val
     * @return
     */
    protected int closestLowerPowerOfTwo(int val)
    {
        int power = (int)Math.floor(log2(val));
        int pow2 = (int)Math.pow(2, power);
        
        if (pow2 > maxSize)
            return maxSize;
        else
            return pow2; 
    }


    /**
     * Calculate closest power of two value higher than argument
     * TODO closestHigherPowerOfTwo method description
     * @param val
     * @return
     */
    protected int closestHigherPowerOfTwo(int val)
    {
        int power = (int)Math.ceil(log2(val));
        return (int)Math.pow(2, power);
    }


    /**
     * Calculate log base 2 of the argument
     * @param val
     * @return
     */
    protected double log2(double val)
    {
        return Math.log(val)/Math.log(2);
    }


    public boolean isNpotSupported()
    {
        return npotSupported;
    }


    public boolean isNormalizationRequired()
    {
        return normalizationRequired;
    }
    
    
    private void logStatistics()
    {
        if (log.isDebugEnabled())
        {
            int texCount = 0;
            
            for (int i=0; i<65535; i++)
                if (gl.glIsTexture(i))
                    texCount++;
            
            log.debug("Num Tex = " + texCount);
        }
    }
}